---
title: HM - 鸿蒙-华为闹钟
---

# 鸿蒙-6. 华为闹钟

## 阶段案例-华为闹钟


|首页|详情页|
|----|----|
|<img src="./images/40.png" width="320" />|<img src="./images/41.png" width="320" />|


### 1. 页面结构-首页结构

```typescript title="pages/Index.ets"
import { CanvasComp } from '../components/CanvasComp'
import { ClockItemComp } from '../components/ClockItemComp'

@Entry
@Component
struct Index {
  build() {
    Stack({ alignContent: Alignment.Bottom }){
      Column({ space: 15 }) {
        Text('闹钟')
          .fontSize('24')
          .width('100%')

        CanvasComp()

        ForEach([1, 2, 3], () => {
          ClockItemComp()
            .onClick(() => {
              router.pushUrl({
                url: 'pages/DetailPage',
              })
            })
        })
      }
      .padding(15)
      .height('100%')
      .width('100%')
      .backgroundColor('#f5f5f5')
      // 添加
      Text('+')
        .width(50)
        .aspectRatio(1)
        .backgroundColor('#06f')
        .fontSize(40)
        .fontWeight(100)
        .fontColor('#fff')
        .borderRadius(25)
        .textAlign(TextAlign.Center)
        .margin({ bottom: 50 })
        .onClick(() => {
          router.pushUrl({
            url: 'pages/DetailPage',
          })
        })
    }
  }
}
```

```typescript title="components/CanvasComp.ets"
@Component
export struct CanvasComp {
  build() {
    
  }
}
```

```typescript title="components/ClockItemComp.ets"
@Component
export struct ClockItemComp {
  build() {
    
  }
}
```

### 2. 页面结构-绘制闹钟

1. 绘制表盘
2. 绘制 时分秒 指针
3. 走起来

``` typescript
@Component
export struct CanvasComp {
  private settings = new RenderingContextSettings(true)
  private context = new CanvasRenderingContext2D(this.settings)
  private panImg = new ImageBitmap('/images/ic_clock_pan.png')
  private hourImg = new ImageBitmap('/images/ic_hour_pointer.png')
  private minuteImg = new ImageBitmap('/images/ic_minute_pointer.png')
  private secondImg = new ImageBitmap('/images/ic_second_pointer.png')

  // 画布尺寸
  canvasSize = 252
  // 指针尺寸
  pointerWidth = 8

  startDraw() {
    this.drawClock()
    setInterval(() => {
      this.drawClock()
    }, 1000)
  }

  /**
   * 绘制闹钟
   */
  drawClock() {
    this.context.clearRect(0, 0, this.canvasSize, this.canvasSize)
    this.context.drawImage(this.panImg, 0, 0, this.canvasSize, this.canvasSize)
    // 根据时间绘制指针
    const date = new Date()
    const hour = date.getHours()
    const minute = date.getMinutes()
    const second = date.getSeconds()
    this.drawPointer(this.hourImg, hour % 12 / 12 * 360)
    this.drawPointer(this.minuteImg, minute / 60 * 360)
    this.drawPointer(this.secondImg, second / 60 * 360)
  }

  /**
   * 绘制指针
   * @param img - 指针图片
   * @param angle - 旋转角度，起点是 Y 轴上方向
   */
  drawPointer(img: ImageBitmap, angle: number) {
    this.context.save()
    this.context.translate(this.canvasSize / 2, this.canvasSize / 2)
    this.context.rotate((angle + 180) * Math.PI / 180)
    this.context.translate(-this.canvasSize / 2, -this.canvasSize / 2)
    this.context.drawImage(img, this.canvasSize / 2 - this.pointerWidth / 2, 0, this.pointerWidth, this.canvasSize)
    this.context.restore()
  }

  build() {
    Canvas(this.context)
      .width(this.canvasSize)
      .aspectRatio(1)
      .onReady(() => {
        this.startDraw()
      })
  }
}
```

### 3. 页面结构-绘制闹钟任务列表

```typescript
@Component
export struct ClockItemComp {
  build() {
    Row(){
      Column({ space: 5 }) {
        Row({ space: 5 }){
          Text('下午')
            .fontColor('#666')
          Text('04:30')
            .fontWeight(600)
            .fontSize(18)
        }
        Row({ space: 15 }){
          Text('闹钟')
            .fontColor('#999')
            .fontSize(14)
          Text('不重复')
            .fontColor('#999')
            .fontSize(14)
        }
      }
      .alignItems(HorizontalAlign.Start)

      // 开关
      Toggle({ type: ToggleType.Switch , isOn: true })
    }
    .height(64)
    .padding({ left: 20, right: 15 })
    .width('100%')
    .backgroundColor('#fff')
    .borderRadius(30)
    .justifyContent(FlexAlign.SpaceBetween)
  }
}
```


### 4. 页面结构-详情页

```typescript
@Entry
@Component
struct DetailPage {

  submit () {
    // TODO
  }

  @Builder
  CellBuilder (title: string, value: string) {
    Row(){
      Text(title)
        .layoutWeight(1)
      Text(value)
        .fontColor('#999')
      Image('/images/ic_public_arrow_right.svg')
        .width(18)
        .aspectRatio(1)
        .fillColor('#999')
    }
    .height(60)
    .padding({ left: 15, right: 15 })
    .backgroundColor('#fff')
  }

  build() {
    Navigation(){
      Column({ space: 15 }){
        TimePicker()
          .borderRadius(16)
          .clip(true)

        Column({ space: 1 }){
          this.CellBuilder('闹铃名称', '闹铃')
          this.CellBuilder('重复', '不重复')
        }
        .borderRadius(16)
        .clip(true)

        Text('删除闹铃')
          .width(100)
          .height(40)
          .borderRadius(20)
          .backgroundColor('#c3c4c5')
          .textAlign(TextAlign.Center)
          .fontColor('#f00')
          .opacity(0.3)
      }
      .padding(15)
    }
    .title('新建闹钟')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
    .backgroundColor('#f5f5f5')
    .menus([
      { value: '', icon: '/images/ic_confirm.png', action: () => this.submit() }
    ])
  }
}
```

### 5. 业务逻辑-首选项存储工具

- 创建首选项存储工具类
- 获取首选项实例
- 设置和修改闹钟信息
- 删除闹钟信息
- 获取所有的闹钟信息

```typescript
import preferences from '@ohos.data.preferences'

export class ClockItem {
  hour?: number
  minute?: number
  id?: string
  reminderId?: number
  enabled?: boolean
}

export class ClockStore {

  getStore() {
    return preferences.getPreferences(getContext(this), 'clockStore')
  }

  async addClock(clockItem: ClockItem) {
    const store = await this.getStore()
    await store.put(clockItem.id, JSON.stringify(clockItem))
    await store.flush()
  }

  async delClock(id: string) {
    const store = await this.getStore()
    await store.delete(id)
    await store.flush()
  }

  async getAllClock() {
    const store = await this.getStore()
    const data = await store.getAll()
    const list = Object.keys(data).map<ClockItem>(key => {
      return JSON.parse(data[key])
    })
    return list
  }
}
```

### 6. 业务逻辑-添加闹钟任务

- 绑定时间选择组件，回收选中后的时间数据
- 开启提醒
- 保存到首选项

```typescript
  @State
  clockItem: ClockItem = {}

  // Date 不能直接使用 @State 装饰器
  @State
  state: { now: Date } = { now: new Date() }
  clockStore = new ClockStore()

  async submit() {

    // 1. 开启提醒
    const hour = this.state.now.getHours()
    const minute = this.state.now.getMinutes()
    const request: reminderAgentManager.ReminderRequestAlarm = {
      reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,
      hour,
      minute,
      title: '闹钟',
      ringDuration: 60
    }
    const reminderId = await reminderAgentManager.publishReminder(request)
    // 2. 记录数据
    this.clockItem = {
      hour,
      minute,
      id: util.generateRandomUUID(),
      reminderId,
      enabled: true
    }
    await this.clockStore.addClock(this.clockItem)
    promptAction.showToast({ message: '添加闹钟成功' })
    router.back()
  } 
```

```typescript
        TimePicker({
          selected: this.state.now
        })
          .borderRadius(16)
          .clip(true)
```

### 7. 业务逻辑-渲染闹钟任务列表

- 获取首选项闹钟数据
- 遍历数组
- 去闹钟项组件渲染

1） 获取首选项闹钟数据
```typescript
  @State
  clockList: ClockItem[] = []

  clockStore = new ClockStore()

  aboutToAppear() {
    this.initData()
  }
  onPageShow() {
    this.initData()
  }
  async initData() {
    this.clockList = await this.clockStore.getAllClock()
  }
```

2） 遍历数组

```typescript
        ForEach(this.clockList, (item: ClockItem) => {
          ClockItemComp({ item })
            .onClick(() => {
              router.pushUrl({
                url: 'pages/DetailPage',
                params: item
              })
            })
        })
```

3） 去闹钟项组件渲染

```typescript
  @State
  item: ClockItem = {}

  padZero(val: number = 0) {
    return val.toString().padStart(2, '0')
  }
```

```typescript
        Row({ space: 5 }) {
          Text(this.item.hour > 12 ? '下午' : '上午')
            .fontColor('#666')
          Text(`${this.padZero(this.item.hour > 12 ? this.item.hour - 12 : this.item.hour)}:${this.padZero(this.item.minute)}`)
            .fontWeight(600)
            .fontSize(18)
        }
```


### 8. 业务逻辑-修改闹铃任务

- 传递闹铃数据到详情页，需要接收下
- 页面上的标题需要判断显示
- 在 submit 函数中加入编辑逻辑


1）传递闹铃数据到详情页，需要接收下

```typescript title="DetailPage.ets"
  aboutToAppear() {
    const params = router.getParams() as ClockItem
    if (params && params.id) {
      this.clockItem = { ...params }
      this.state.now.setHours(this.clockItem.hour)
      this.state.now.setMinutes(this.clockItem.minute)
    }
  }
```

2）页面上的标题需要判断显示

```diff title="DetailPage.ets"
+    .title((this.clockItem.id ? '编辑' : '添加') + '闹钟')
    .titleMode(NavigationTitleMode.Mini)
    .mode(NavigationMode.Stack)
```

3）在 submit 函数中加入编辑逻辑

```typescript title="DetailPage.ets"
      // 2. 记录数据
      if (this.clockItem.id) {
        this.clockItem.hour = hour
        this.clockItem.minute = minute
        this.clockItem.enabled = true
        // 不加 await 让它异步执行，因为可能存在失效的代理提醒，这样不会阻碍后续逻辑
        reminderAgentManager.cancelReminder(this.clockItem.reminderId)
        this.clockItem.reminderId = reminderId
        await this.clockStore.addClock(this.clockItem)
        promptAction.showToast({ message:'编辑闹钟成功' })
      } else {
        this.clockItem = {
          hour,
          minute,
          id: util.generateRandomUUID(),
          reminderId,
          enabled: true
        }
        await this.clockStore.addClock(this.clockItem)
        promptAction.showToast({ message:'添加闹钟成功' })
      }
```

### 9. 业务逻辑-删除闹钟任务

- 删除按钮在编辑条件下展示，绑定事件函数
- 实现删除功能


1）删除按钮在编辑条件下展示，绑定事件函数

```typescript title="DetailPage.ets"
        if ( this.clockItem.id ) {
          Text('删除闹铃')
            .width(100)
            .height(40)
            .borderRadius(20)
            .backgroundColor('#c3c4c5')
            .textAlign(TextAlign.Center)
            .fontColor('#f00')
            .opacity(0.3)
            .onClick(() => {
              this.remove()
            })
        }
```

2）实现删除功能

```typescript
  async remove () {
    try {
      await this.clockStore.delClock(this.clockItem.id)
      promptAction.showToast({ message: '删除闹钟成功' })
      // 不加 await 让它异步执行，因为可能存在失效的代理提醒，这样不会阻碍跳转
      reminderAgentManager.cancelReminder(this.clockItem.reminderId)
      router.back()
    } catch (e) {
      console.error('CLOCK-REMOVE', e.message)
    }
  }
```

### 10. 业务逻辑-通知唤起应用和关闭延时

- 抽取创建后台代理工具类
- 加入唤起配置，加入延时和关闭配置
- 进行复用

1）加入唤起配置，加入延时和关闭配置

```typescript title="utils/alarmManager.ets"
      // 跳转
      wantAgent: {
        pkgName: 'com.itcast.myapplication',
        abilityName: 'EntryAbility'
      },
      // 操作按钮
      actionButton: [
        { title: '关闭', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE },
        { title: '延时提醒', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE }
      ],
      snoozeTimes: 1,
      timeInterval: 10 * 60
```


2）抽取创建后台代理工具类

```typescript title="utils/alarmManager.ets"

import reminderAgentManager from '@ohos.reminderAgentManager'
export class AlarmManager {

  // 添加代理提醒
  static async addReminder (hour: number, minute: number) {
    // 1. 添加后台代理提醒-闹钟
    const reminderRequest: reminderAgentManager.ReminderRequestAlarm = {
      reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,
      hour,
      minute,
      ringDuration: 60,
      title: '闹铃',
      // 跳转
      wantAgent: {
        pkgName: 'com.itcast.hmday05',
        abilityName: 'EntryAbility'
      },
      // 延时和关闭
      actionButton: [
        { title: '关闭', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE },
        { title: '延时提醒', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE }
      ],
      snoozeTimes: 2,
      timeInterval: 10 * 60
    }

    const reminderId = await reminderAgentManager.publishReminder(reminderRequest)
    return reminderId
  }
}

```


3）进行复用

```diff title="pages/DetailPage.ets"
      // 1. 开启提醒
      const hour = this.state.now.getHours()
      const minute = this.state.now.getMinutes()
+      const reminderId = await AlarmManager.addReminder(hour, minute)
      // 2. 记录数据
```


### 11. 业务逻辑-停止和开启闹钟任务

```typescript title="ClockItemComp.ets"
  clockStore = new ClockStore()

  async toggle(isOn: boolean) {
    reminderAgentManager.cancelReminder(this.item.reminderId)

    this.item.enabled = isOn
    if (this.item.enabled) {
      const reminderId = await AlarmManager.createAlarmTask(this.item.hour, this.item.minute)
      this.item.reminderId = reminderId
    }
    this.clockStore.addClock(this.item)
    promptAction.showToast({ message: isOn ? '已开启' : '已关闭' })
  }
```

```typescript title="ClockItemComp.ets"
      // 开关
      Toggle({ type: ToggleType.Switch, isOn: this.item.enabled })
        .onChange(isOn => this.toggle(isOn))
```

